<template>
  <div class="echarts-container" :data-echarts-id="id">
    <div class="echarts-prepend" v-show="$slots.prepend">
      <slot name="prepend"/>
    </div>
    <div class="echarts-instance" ref="chart"></div>
    <div class="echarts-append" v-show="$slots.append">
      <slot name="append"/>
    </div>
    <div class="echarts-empty-mask" v-if="showEmptyMask" v-show="dataIsEmpty">
      <slot name="empty">
        <span>{{ emptyText }}</span>
      </slot>
    </div>
  </div>
</template>

<script>
import cache from './cache'
import utils from './utils'
import carousel from '@/components/carousel'
import thirdparts from '@/components/thirdparts'

export default {
  name: 'ECharts',
  mixins: [carousel],
  props: {
    options: {
      required: true
    },
    id: {
      type: String
    },
    /**
     * 用于 echarts.connect/disconnect 的 group 名称
     */
    group: {
      type: String
    },
    /**
     * 应用的主题。可以是一个主题的配置对象，也可以是使用已经通过 echarts.registerTheme 注册的主题名称。
     */
    theme: {
      type: [String, Object]
    },
    /**
     * 附加参数
     */
    opts: {
      type: Object
    },
    optionOpts: {
      type: Object
    },
    // 当指定了 `option-opts` 时无效
    notMerge: {
      type: Boolean,
      default: false
    },
    // 当指定了 `option-opts` 时无效
    lazyUpdate: {
      type: Boolean,
      default: false
    },
    /**
     * 需要监听的事件
     * @see https://www.echartsjs.com/zh/api.html#events
     */
    listen: {
      type: Object
    },
    /**
     * 是否在窗口/容器大小变化后自动执行 resize 操作
     */
    autoResize: {
      type: Boolean,
      default: true
    },
    /**
     * 没有数据时显示的文本
     */
    emptyText: {
      type: String,
      default: '暂无数据'
    },
    /**
     * 是否在没有数据时显示 mask
     */
    showEmptyMask: {
      type: Boolean,
      default: false
    },
    // 延迟渲染
    delay: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      cid: this.id || `vec-${Math.round(Math.random() * 1000)}-${new Date().getTime()}`,
      isReady: false,
      eventHandlers: {},
      // 记录上次渲染时使用的 key
      // 以在必要的时候设置 notMerge
      lastKey: undefined,
      readyCallbacks: []
    }
  },
  mounted() {
    this.render()
  },
  watch: {
    options: {
      deep: true,
      handler() {
        this.render()
      }
    }
  },
  computed: {
    /**
     *
     * @return 当 options 为空时，返回 null
     */
    computedOption() {
      if (!this.options || !Object.keys(this.options).length) {
        return null
      }
      const option = {
        ...cache.defaults,
        ...this.options
      }

      this.toArray(option, 'legend')
      this.toArray(option, 'geo')
      this.toArray(option, 'geo3D')
      this.toArray(option, 'series')

      return option
    },
    dataIsEmpty() {
      return utils.isEmpty(this.computedOption)
    },
    eventsList() {
      const events = []
      for (const eventName in this.listen) {
        let e = this.listen[eventName]
        if (!e) {
          continue
        }
        if (typeof e === 'function') {
          e = {
            name: eventName,
            handler: e
          }
        } else if (!e.name) {
          e.name = eventName
        }
        events.push(e)
      }
      return events
    },
    computedOptionOpts() {
      if (this.opts) {
        return this.opts
      }
      return {
        notMerge: this.notMerge,
        lazyUpdate: this.lazyUpdate
      }
    }
  },
  methods: {
    async getChart() {
      const echarts = await utils.loadLibrary()
      if (!this.$refs.chart) {
        return null
      }
      return echarts.getInstanceByDom(this.$refs.chart)
    },
    /**
     * 渲染图表
     */
    async render() {
      let option = this.computedOption

      let chart = await this.getChart()

      // 选项为空时，清空图表
      if (!option || this.dataIsEmpty) {
        if (chart) {
          chart.clear()
        }
        return
      }

      await thirdparts.load(option)

      if (!chart) {
        if (!this.$refs.chart) {
          return
        }
        const echarts = await utils.loadLibrary()
        chart = echarts.init(this.$refs.chart, this.theme, this.opts)
        if (this.group) {
          chart.group = this.group
        }
        this.bindEvents()
      }

      await utils.wait(this.delay)
      const opts = Object.assign({}, this.computedOptionOpts)
      // 当 key 变化时，表示不是同一个图表了，强制清除原有数据
      let notMerge = option.key !== this.lastKey
      if (notMerge) {
        opts.notMerge = true
        this.lastKey = option.key
      }
      delete option.key

      const interceptor = cache.getInterceptor()
      if (interceptor) {
        option = interceptor(option)
      }
      chart.setOption(option, opts)
      this.$nextTick(() => {
        cache.add(this)
        this.emitReadyEvent()
      })
    },
    async bindEvents() {
      if (!this.eventsList.length) {
        return
      }
      const chart = await this.getChart()
      if (!chart) {
        return
      }
      for (const {name, query, handler, context} of this.eventsList) {
        if (query && context) {
          chart.on(name, query, handler, context)
          continue
        }
        if (context) {
          chart.on(name, handler, context)
          continue
        }
        chart.on(name, handler)
      }
    },
    async unbindEvents() {
      const chart = await this.getChart()
      if (!chart) {
        return
      }
      for (const {name, handler} of this.eventsList) {
        chart.off(name, handler)
      }
    },
    async emitReadyEvent() {
      if (this.isReady) {
        return
      }
      this.isReady = true
      const chart = await this.getChart()
      this.$emit('ready', {id: this.cid, chart})
      for (const callback of this.readyCallbacks) {
        callback(chart)
      }
    },
    toArray(obj, prop) {
      const value = obj[prop]
      if (!value) {
        return
      }

      obj[prop] = Array.isArray(value) ? value : [value]
    }
  },
  beforeDestroy() {
    cache.remove(this.cid)
    this.unbindEvents()
    this.getChart().then(chart => {
      if (chart) {
        chart.dispose()
      }
    })
  }
}
</script>

<style lang="less">
.echarts-container {
  position: relative;
  height: 100%;
}

.echarts-prepend,
.echarts-instance,
.echarts-append,
.echarts-empty-mask {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.echarts-prepend {
  z-index: 1;
}

.echarts-instance {
  z-index: 2;
}

.echarts-append {
  z-index: 3;
  pointer-events: none;

  & > * {
    pointer-events: initial;
  }
}

.echarts-toolbar {
  position: absolute;
  right: 5px;
  top: 5px;
  z-index: 4;
}

.echarts-empty-mask {
  opacity: 0.8;
  font-size: large;
  text-align: center;
  justify-content: center;
  align-items: center;
  display: flex;
  z-index: 9;
}
</style>
